﻿
using System;
using System.Linq;
using System.Xml.Linq;
using Devtm.Generator.Writers;
using Devtm.Generator.Contexts;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using Devtm.Writers;
using Devtm.Parsing;
using System.Reflection;

namespace Devtm.Generator.Writers
{


    public class TemplateGenerator : IDisposable
    {


        protected const string Joker = "*";

        public const string XmlNamespace = @"http://devtm.fr/transform/2012/01/";


        public void Dispose()
        {
            Dispose(true);
        }


        protected virtual void Dispose(bool disposing)
        {
            
        }


        ~TemplateGenerator()
        {
            Dispose(false);
        }


    }


    public class TemplateGenerator<TContext> : TemplateGenerator, IParsingVisitor
        where TContext : IContext, new()
    {


        protected TContext Context;
        private Stack<string> location = new Stack<string>();      
        private OutParameters Out;
        private Hashtable _cache = new Hashtable();        
        protected Hashtable _types = new Hashtable();
        protected Dictionary<string, object> _instances = new Dictionary<string, object>();


        public TemplateGenerator(TContext context)
        {
            this.Context = context;
            this.Parser = new Parser(_cache, this);
        }


        #region Visitor


        public virtual void Visit(XElement xtransform)
        {

            Log(Common.LogLevelEnm.Trace, "enter in method {0}", MethodInfo.GetCurrentMethod());

            XElement xdata = null;
            foreach (var item in xtransform.Attributes())
            {

                if (item.Name.Namespace == "http://www.w3.org/2000/xmlns/")
                    _cache.Add(item.Name.LocalName, item.Value);

                else if (item.Name.LocalName == "path")
                {

                    var vv = item.Value;
                    string value = this.Parser.Evaluate(vv, xtransform);

                    if (System.IO.File.Exists(value))
                        xdata = XElement.Load(value);

                    else
                        throw new System.IO.FileNotFoundException(value);

                }

            }

            if (xdata == null)
                throw new System.Xml.XmlException(string.Format("missing attribute path"));

            VisitChildren(xtransform, xdata, true);
            VisitChildren(xtransform, xdata);

        }


        public virtual void VisitChildren(XElement xtransform, XElement xdata, bool firstParsing = false)
        {

            foreach (var item in xtransform.Elements())
                if (item.Name.Namespace.NamespaceName == XmlNamespace)
                    Visit(item, xdata, firstParsing);

        }


        private void Visit(XElement xtransform, XElement xdata, bool firstParsing = false)
        {
            switch (xtransform.Name.LocalName)
            {

                #region Mapping

                case "mappings":
                    if (firstParsing)
                        VisitMappings(xtransform, xdata);
                    break;

                case "path":
                    VisitPath(xtransform, xdata);
                    break;

                case "pathSourceProvider":
                    VisitPathSourceProvider(xtransform, xdata);
                    break;

                case "pathValue":
                    VisitPathValue(xtransform, xdata);
                    break;

                #endregion

                #region Locations

                case "out":
                    if (!firstParsing)
                        VisitOut(xtransform, xdata);
                    break;
                case "file":
                    if (!firstParsing)
                        VisitFile(xtransform, xdata);
                    break;
                case "folder":
                    VisitFolder(xtransform, xdata);
                    break;
                case "select":
                    VisitSelect(xtransform, xdata);
                    break;
                case "where":
                    VisitWhere(xtransform, xdata);
                    break;
                case "template":
                    VisitTemplate(xtransform, xdata);
                    break;
                case "var":
                    VisitVariable(xtransform, xdata);
                    break;

                #endregion

                default:
                    throw new NotImplementedException(xtransform.Name.LocalName);
            }


        }


        private void VisitMappings(XElement item, XElement xdata)
        {
            VisitChildren(item, xdata);
        }


        MappingtargetMatch _currentMatchingTarget;
        private Dictionary<string, MappingPath> _mappings = new Dictionary<string, MappingPath>();
        private void VisitPath(XElement xmapping, XElement xdata)
        {

            Log(Common.LogLevelEnm.Trace, "enter in method {0}", MethodInfo.GetCurrentMethod());

            MappingPath p;

            string sourceItem = AttributeValue(xmapping, xdata, XName.Get("targetItem", XmlNamespace));
            if (_mappings.ContainsKey(sourceItem))
                p = _mappings[sourceItem];
            else
                _mappings.Add(sourceItem, (p = new MappingPath() { SourceITem = sourceItem.ToName() }));


            string targetMatch = AttributeValue(xmapping, xdata, XName.Get("targetMatch", XmlNamespace));
            string match = AttributeValue(xmapping, xdata, XName.Get("match", XmlNamespace));
            string keyTargetMatch = targetMatch + (match == Joker ? string.Empty : match);
            if (p.SourceMatchs.ContainsKey(keyTargetMatch))
                _currentMatchingTarget = p.SourceMatchs[keyTargetMatch];
            else p.SourceMatchs.Add(keyTargetMatch, (_currentMatchingTarget = new MappingtargetMatch() { SourceMatch = targetMatch.ToName(), Match = match }));

            
            foreach (XElement item in xdata.DescendantsAndSelf(p.SourceITem))
            {

                XAttribute attr;

                if (match == Joker || ((attr = item.Attribute(_currentMatchingTarget.SourceMatch)) != null && attr.Value == match))
                {
                    VisitChildren(xmapping, item);
                }

            }
        
        }


        private void VisitPathSourceProvider(XElement xtransform, XElement xdata)
        {

            Log(Common.LogLevelEnm.Trace, "enter in method {0}", MethodInfo.GetCurrentMethod());

            object run = null;

            var _type = AttributeValue(xtransform, xdata, XName.Get("type", XmlNamespace), "");

            if (!_instances.ContainsKey(_type))
            {

                Type type = null;
                if (_types.ContainsKey(_type))
                    type = _types[_type] as Type;
                else
                {
                    type = GetType().Assembly.GetType(_type);
                    _types.Add(_type, type);
                }

                if (type == null)
                    throw new Exception(string.Format("the template '{0}' can't be found", _type));

                run = Activator.CreateInstance(type, new object[] { Context });
                _instances.Add(_type, run);

            }
            else
            {
                run = _instances[_type];
            }

            _currentMatchingTarget.Provider = run;
            _currentMatchingTarget.Mode = ModeEnum.Provider;

        }


        private void VisitPathValue(XElement xtransform, XElement xdata)
        {
            Log(Common.LogLevelEnm.Trace, "enter in method {0}", MethodInfo.GetCurrentMethod());
            _currentMatchingTarget.Value = AttributeValue(xtransform, xdata, XName.Get("value", XmlNamespace), "");
            _currentMatchingTarget.Mode = ModeEnum.Value;
        }
 

        private void VisitVariable(XElement xtransform, XElement xdata)
        {

            Log(Common.LogLevelEnm.Trace, "enter in method {0}", MethodInfo.GetCurrentMethod());

            var name = AttributeValue(xtransform, xdata, XName.Get("name", XmlNamespace));
            var value = AttributeValue(xtransform, xdata, XName.Get("value", XmlNamespace));
        
            string _k = "__" + name;
            Stack<string> st = null;

            if (!_cache.ContainsKey(_k))
            {
                st = new Stack<string>();
                _cache.Add(_k, st);
            }
            else
                st = _cache[_k] as Stack<string>;

            st.Push(value);

            VisitChildren(xtransform, xdata);

            st.Pop();

        }


        private void VisitFolder(XElement xtransform, XElement xdata)
        {

            Log(Common.LogLevelEnm.Trace, "enter in method {0}", MethodInfo.GetCurrentMethod());

            var loc = AttributeValue(xtransform, xdata, XName.Get("location", XmlNamespace), string.Empty);
            if (!string.IsNullOrEmpty(loc))
                location.Push(loc);

            VisitChildren(xtransform, xdata);

            location.Pop();
            
        }


        private void VisitOut(XElement xtransform, XElement xdata)
        {

            Log(Common.LogLevelEnm.Trace, "enter in method {0}", MethodInfo.GetCurrentMethod());

            if (this.Out != null)
                throw new FormatException("out unexpected. the out must be in transform balise");


            var project = AttributeValue(xtransform, xdata, XName.Get("project", XmlNamespace), Context.DefaultProject.ProjectFile.Name);


            this.Out = new OutParameters()
            {
                Content = AttributeValue(xtransform, xdata, XName.Get("content", XmlNamespace)),
                Project = Context.Solution.GetProjectByFilename(project),
            };


            var loc = AttributeValue(xtransform, xdata, XName.Get("location", XmlNamespace), "");
            if (!string.IsNullOrEmpty(loc))
                location.Push(loc);

            VisitChildren(xtransform, xdata);

            if (!string.IsNullOrEmpty(loc))
                location.Pop();

            this.Out = null;

        }


        private void VisitSelect(XElement xtransform, XElement xdata)
        {

            Log(Common.LogLevelEnm.Trace, "enter in method {0}", MethodInfo.GetCurrentMethod());

            XName el = AttributeValue(xtransform, xdata, XName.Get("element", XmlNamespace)).ToName();

            foreach (var item in xdata.DescendantsAndSelf(el))
                VisitChildren(xtransform, item);

        }


        private void VisitWhere(XElement xtransform, XElement xdata)
        {

            Log(Common.LogLevelEnm.Trace, "enter in method {0}", MethodInfo.GetCurrentMethod());

            var _v = AttributeValue(xtransform, xdata, XName.Get("attribute", XmlNamespace));

            if (_v == null)
                throw new FormatException(string.Format("attribute 'attribute' not found"));

            string match = AttributeValue(xtransform, xdata, XName.Get("match", XmlNamespace));
            string typeMatch = AttributeValue(xtransform, xdata, XName.Get("typeMatch", XmlNamespace), "verbatim");

            bool test = false;
            switch (typeMatch)
            {

                case "regex":
                    test = System.Text.RegularExpressions.Regex.IsMatch(_v, match);
                    break;

                case "verbatim":
                    test = match == _v;
                    break;

                default:
                    throw new FormatException(string.Format("typeMatch {0}", typeMatch));

            }

            if (test)
                VisitChildren(xtransform, xdata);

        }


        private void VisitFile(XElement xtransform, XElement xdata)
        {

            Log(Common.LogLevelEnm.Trace, "enter in method {0}", MethodInfo.GetCurrentMethod());

            Context.Filename = AttributeValue(xtransform, xdata, XName.Get("filename", XmlNamespace), "");

            Context.Extend = AttributeValue(xtransform, xdata, XName.Get("extend", XmlNamespace), "");

            if (Context.Filename == null)
                throw new FormatException(string.Format("attribute filename not found"));

            Context.Namespace = GetLocation();

            VisitChildren(xtransform, xdata);

        }


        private string GetLocation()
        {

            StringBuilder s = new StringBuilder();

            foreach (var item in location.Reverse())
            {

                var t = item.Replace("/", "\\").Replace(".", "\\").Split('\\');

                foreach (var item2 in t)
                {
                    string s1 = item2.Trim();
                    if (!string.IsNullOrEmpty(s1))
                    {
                        s.Append("\\");
                        s.Append(s1);
                    }
                }

            }

            if (s.Length > 0)
                s.Remove(0, 1);

            return s.ToString();
        }

        
        private void VisitTemplate(XElement xtransform, XElement xdata)
        {

            Log(Common.LogLevelEnm.Trace, "enter in method {0}", MethodInfo.GetCurrentMethod());

            IRun run = null;
            var _type = AttributeValue(xtransform, xdata, XName.Get("type", XmlNamespace), string.Empty);

            if (_instances.ContainsKey(_type))
                run = _instances[_type] as IRun;

            else
            {

                Type type = null;
                if (_types.ContainsKey(_type))
                    type = _types[_type] as Type;
                else
                {
                    type = GetType().Assembly.GetType(_type);
                    _types.Add(_type, type);
                }

                if (type == null)
                    throw new Exception(string.Format("the template '{0}' can't be found", _type));

                run = Activator.CreateInstance(type, new object[] { Context }) as IRun;
                _instances.Add(_type, run);

            }

            run.Run(new object[] { xdata, _mappings });


        }

        #endregion


        #region private

        [System.Diagnostics.DebuggerStepThrough]
        private T AttributeValue<T>(XElement xitem, XElement xdata, XName key, Func<string, T> f, string defaultValue = null)
        {
            string value = AttributeValue(xitem, xdata, key, defaultValue);
            T result = f(value);
            return result;
        }

        [System.Diagnostics.DebuggerStepThrough]
        private string AttributeValue(XElement xitem, XName key, string defaultValue = null)
        { 
            return AttributeValue(xitem, xitem, key, defaultValue);
        }

        [System.Diagnostics.DebuggerStepThrough]
        private string AttributeValue(XElement xitem, XElement xdata, XName key, string defaultValue = null)
        {

            string result = string.Empty;

            XAttribute attribute = xitem.Attribute(key);

            if (attribute != null)
                result = attribute.Value;

            else if (defaultValue != null)
                result = defaultValue;

            else
                throw new MissingFieldException(string.Format("missing attribute '{0}' on the element '{1}'", key, xitem.Name));


            try
            {
                result = this.Parser.Evaluate(result, xdata);
            }
            catch (Exception e)
            {

                string v = xitem.ToString();
                if (v.Length > 50)
                    v = v.Substring(0, 50);
                throw new Exception(string.Format("Fail to evaluate \"...{0}\"", v), e);
            }


            return result;

        }

        #endregion


        protected Parser Parser { get; private set; }


        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
            if (disposing)
            {

            }
        }


        #region IParsingVisitor implementation

        public Generator.Solution Solution { get { return Context.Solution; } }

        #endregion


        protected void Log(Devtm.Common.LogLevelEnm level, string log, params object[] parameters)
        {
            var p = parameters.ToList();
            p.Add(Devtm.Common.Tags.BaseTemplateGenerator);
            Devtm.Common.Logger.Log(this, level, log, p);
        }

        protected void Log(Exception exception, params object[] parameters)
        {
            var p = parameters.ToList();
            p.Add(Devtm.Common.Tags.BaseTemplateGenerator);
            Devtm.Common.Logger.Log(this, Devtm.Common.LogLevelEnm.Exception, exception, p);
        }
    }
}
